Skip to content

Feature/rbac role enforcement#250

Merged
Calebux merged 8 commits intoCalebux:mainfrom
fawizzy:feature/rbac-role-enforcement
Mar 29, 2026
Merged

Feature/rbac role enforcement#250
Calebux merged 8 commits intoCalebux:mainfrom
fawizzy:feature/rbac-role-enforcement

Conversation

@fawizzy
Copy link
Copy Markdown
Contributor

@fawizzy fawizzy commented Mar 28, 2026

Problem

The authenticate middleware read user.user_metadata?.role from the JWT but
never attached it to req.user, so no route or middleware could check it. Any
authenticated user — regardless of role — could perform destructive operations
like deleting subscriptions or managing team members.

Changes

middleware/auth.ts

  • Exported UserRole type ('owner' | 'admin' | 'member' | 'viewer')
  • Added role: UserRole to AuthenticatedRequest.user
  • Both authenticate and optionalAuthenticate now extract the role from JWT
    user_metadata, validate it, and attach it to req.user (defaults to 'member' if
    absent or unrecognized)

middleware/rbac.ts (new)

  • ROLE_PERMISSIONS map defining permission scope per role
  • requireRole(...roles: UserRole[]) middleware factory — returns 403 if the
    caller's JWT role is not in the allowed list

routes/subscriptions.ts

  • DELETE /:id — now requires owner or admin
  • POST /bulk — now requires owner or admin

routes/team.ts

  • POST /invite — now requires owner or admin
  • GET /pending — now requires owner or admin
  • PUT /:memberId/role — now requires owner
  • DELETE /:memberId — now requires owner or admin

Notes

  • Team routes retain their existing DB-level canManageTeam() / isOwner checks
    as a second layer of defense
  • requireRole runs before any DB query, failing fast at the JWT level
  • Unknown or missing roles in the JWT default to 'member' (least privilege)

Acceptance Criteria

fawizzy added 6 commits March 25, 2026 10:50
The CSRF functions (generateCSRFToken, storeCSRFToken, getCSRFToken,
validateCSRFToken) were never called anywhere in the codebase and the
backend never validated any CSRF header. Since auth uses Supabase JWT
Bearer tokens — not cookies — CSRF is not a risk; removed the dead code
and added a comment explaining why. Closes Calebux#120.
- Switch Geist font import from Google Fonts to local geist npm package
  (network can't reach fonts.googleapis.com at build time)
- Remove duplicate logTeamAction/logDataExport functions and stray
  closing braces introduced by upstream merge in audit-log.ts
- Fix private flushAuditQueue called outside class (make public)
- Disable ESLint during builds (pre-existing lint errors across codebase)
- Update lockfiles for new uuid and @types/uuid dependencies
- Add UserRole type and role field to AuthenticatedRequest
- Read role from JWT user_metadata in authenticate/optionalAuthenticate
- Create requireRole middleware with ROLE_PERMISSIONS map
- Apply requireRole to destructive subscription operations (delete, bulk)
- Apply requireRole to team management endpoints (invite, pending, role update, remove)
@drips-wave
Copy link
Copy Markdown

drips-wave bot commented Mar 28, 2026

@fawizzy Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Calebux added a commit that referenced this pull request Mar 29, 2026
Calebux added a commit that referenced this pull request Mar 29, 2026
@Calebux Calebux merged commit 14a09ea into Calebux:main Mar 29, 2026
2 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

User roles/RBAC is a stub — role field is read from JWT but never enforced in any route

2 participants